Skip to content

Conversation

@Ayyanchira
Copy link
Member

@Ayyanchira Ayyanchira commented Sep 30, 2025

🔹 Jira Ticket(s)

✏️ Description

This PR refactors the in-app message rendering system to improve positioning stability and reliability, particularly during device rotations and dynamic content resizing. The core change migrates from a single RelativeLayout to a FrameLayoutRelativeLayoutWebView hierarchy for better positioning control across TOP/CENTER/BOTTOM/FULLSCREEN layouts.

Key Changes

1. Layout Hierarchy Refactor

  • Before: Single RelativeLayout with setVerticalGravity() and MATCH_PARENT sizing
  • After: FrameLayout (positioning) → RelativeLayout (WebView wrapper) → WebView (content)
  • FrameLayout handles dialog positioning via LayoutParams.gravity
  • WebView starts with WRAP_CONTENT and resizes to content height
  • Added applyWindowGravity() method to consistently set window positioning across lifecycle stages

2. Resize Debouncing & Validation

  • Added 200ms debounce delay to prevent resize thrashing during rapid WebView content changes
  • Added height validation to skip resizes for invalid heights (≤ 0) or unchanged heights
  • Tracks lastContentHeight to avoid unnecessary layout passes
  • Prevents multiple resize operations from queuing up

3. Improved Orientation Handling

  • Orientation changes now trigger only on significant rotations (90-degree increments)
  • Increased stabilization delay from 1000ms to 1500ms to allow layout to fully settle
  • Added lastOrientation tracking to prevent spurious resize triggers

4. WebChromeClient Optimization

  • Changed from triggering resize on every progress update to only at 100% page load
  • Reduces resize calls from 20+ per page load to 1-2 times
  • Works in tandem with debouncing for smoother rendering

5. Test Improvements

  • Fixed flaky IterableApiResponseTest by explicitly starting MockWebServer
  • Increased timeout for async operations from 1s to 5s for better CI reliability

Technical Details

Window Gravity Strategy:

  • Gravity is applied at three lifecycle points: onCreateDialog(), onCreateView(), and onStart()
  • Ensures consistent positioning even if Android's window system resets attributes
  • Window is set to MATCH_PARENT × MATCH_PARENT to allow flexible WebView positioning

Resize Flow:

  1. WebView content loads → triggers runResizeScript()
  2. Debounce delays execution by 200ms
  3. performResizeWithValidation() checks height validity
  4. For non-fullscreen: Sets WebView to explicit size with RelativeLayout rules (TOP/CENTER/BOTTOM alignment)
  5. Forces layout updates on WebView and parent container

🧪 Testing Strategy

Automated Tests

  • ✅ Existing unit tests for getVerticalLocation() still pass
  • ✅ Existing test for resize-after-dismiss behavior maintained
  • ⚠️ Need to add: Layout hierarchy validation tests
  • ⚠️ Need to add: Debounce behavior tests
  • ⚠️ Need to add: Concurrent resize call tests

Manual Testing Steps

Prerequisites

  • Test device (physical device recommended for orientation testing)
  • Sample in-app messages for each layout type (TOP, CENTER, BOTTOM, FULLSCREEN)

Test Case 1: Layout Positioning

  1. Launch app and trigger TOP in-app
    • Expected: In-app appears at top of screen, properly sized
  2. Dismiss and trigger CENTER in-app
    • Expected: In-app appears centered vertically and horizontally
  3. Dismiss and trigger BOTTOM in-app
    • Expected: In-app appears at bottom of screen
  4. Dismiss and trigger FULLSCREEN in-app
    • Expected: In-app covers entire screen

Pass Criteria: All layouts appear in correct position without jumping or flickering

Test Case 2: Device Rotation Stability

  1. Trigger CENTER in-app in portrait mode
    • Expected: In-app appears centered
  2. Rotate device to landscape
    • Expected: In-app smoothly resizes and remains centered (no flickering or jumping)
  3. Rotate back to portrait
    • Expected: In-app smoothly resizes back to portrait dimensions
  4. Rapidly rotate device 3-4 times
    • Expected: In-app remains stable, no crashes, no layout corruption

Pass Criteria: In-app maintains correct positioning through all rotations without visual glitches

Test Case 3: Dynamic Content Resizing

  1. Trigger in-app with minimal content (e.g., small banner with text)
    • Expected: WebView sized to content, no excessive whitespace
  2. Trigger in-app with large content (e.g., full hero image + text)
    • Expected: WebView properly sized, content fully visible
  3. Trigger in-app with slowly loading images
    • Expected: In-app resizes smoothly as images load (no multiple "jumps")

Pass Criteria: Content-based resizing is smooth with minimal visual disruption

Test Case 4: Edge Cases

  1. Trigger in-app, immediately dismiss (before content loads)
    • Expected: No crash, clean dismissal
  2. Trigger in-app, rotate device while content is loading
    • Expected: No crash, content loads and positions correctly
  3. Trigger in-app, rapidly scroll device orientation sensor (shake device)
    • Expected: No excessive resize calls, no jank, debouncing prevents thrashing

Pass Criteria: App remains stable under stress conditions

Test Case 5: Regression Check

  1. Test all existing in-app campaigns that were working before
    • Expected: All continue to work without regression
  2. Test on devices with notches/punch-holes
    • Expected: In-apps respect safe areas and don't overlap system UI
  3. Test on tablets in both orientations
    • Expected: In-apps scale appropriately and maintain positioning

Pass Criteria: No regressions in previously working functionality


🚨 Known Issues / Follow-ups

  1. Resize logic sets RelativeLayout params but doesn't update FrameLayout container params - May cause positioning inconsistencies in some edge cases
  2. Multiple applyWindowGravity() calls may cause flickering on some devices - Consider consolidating to single call in onStart()
  3. Debounce state is not thread-safe - Low risk, but should add synchronization for production hardening
  4. Handler cleanup in onDestroy() only removes specific runnable - Should use removeCallbacksAndMessages(null) for complete cleanup

📸 Before/After Screenshots

TODO: Add screenshots showing:

  • TOP/CENTER/BOTTOM layouts before and after
  • Rotation behavior comparison
  • Any visual improvements

🔍 Reviewer Notes

  • Heavy focus on layout positioning logic in IterableInAppFragmentHTMLNotification.java
  • Pay special attention to resize() method (lines 558-643) - this is where FrameLayout/RelativeLayout coordination happens
  • Debouncing logic in runResizeScript() and performResizeWithValidation() should be reviewed for thread safety
  • Orientation listener changes reduce unnecessary resize triggers - verify this doesn't miss legitimate orientation changes

Needs reduction in code. Lot of repition. Heavy review and testing needed. Committing as inapps are looking stable and are sticking with the layout with back to back phone rotations when inapp performance would fail usually
private String messageId;

// Resize debouncing fields
private Handler resizeHandler = new Handler();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Handler.Handler
should be avoided because it has been deprecated.
lastOrientation = currentOrientation;

// Use longer delay for orientation changes to allow layout to stabilize
final Handler handler = new Handler();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Handler.Handler
should be avoided because it has been deprecated.
@Ayyanchira Ayyanchira force-pushed the MOB-11663-InApp-Calculations branch from 2cc8a75 to 8e15c0a Compare October 29, 2025 21:13
Copy link
Member Author

@Ayyanchira Ayyanchira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial pass done

Comment on lines +212 to +215
if (loaded && webView != null) {
// Only trigger on significant orientation changes (90 degree increments)
int currentOrientation = ((orientation + 45) / 90) * 90;
if (currentOrientation != lastOrientation && lastOrientation != -1) {
lastOrientation = currentOrientation;

// Use longer delay for orientation changes to allow layout to stabilize
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows for resize script to be called only limted number of times. This method when implemented, felt it was only called when landscape to portrait rotation was made. Hence explicit conditioning is required.

And removed unnecessary logging
@Ayyanchira Ayyanchira force-pushed the MOB-11663-InApp-Calculations branch from 54b0276 to e85d78a Compare October 30, 2025 21:25
@Ayyanchira Ayyanchira marked this pull request as ready for review October 30, 2025 22:02
@sumeruchat
Copy link
Collaborator

sumeruchat commented Oct 31, 2025

@Ayyanchira Hmm lets have a chat about these. It seems we have made too many changes related to In app screen size for this release and i want to see them all in one place somehow. Also wondering if a smaller diff will solve the problem. cc @davidtruong

Copy link
Contributor

@davidtruong davidtruong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests around orientation calculations

}, 1000);
if (loaded && webView != null) {
// Only trigger on significant orientation changes (90 degree increments)
int currentOrientation = ((orientation + 45) / 90) * 90;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful with the unit conversions since you are using an int and not a double.

Lets pull this calculation out to its own function so we can properly put tests around it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point actually especilly /90s can give easy values around 0 in decimals. Rounding up can be quite errorsome!

Copy link
Collaborator

@sumeruchat sumeruchat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do another code review with AI and also add more tests to validate the fixes.

  1. View Hierarchy Mismatch During Resize
    Risk Level: 🔥 CRITICAL
    Issue: In resize() method line 605, you're creating RelativeLayout.LayoutParams for the WebView, but the WebView's actual parent is a RelativeLayout inside a FrameLayout. You're setting rules like CENTER_IN_PARENT, ALIGN_PARENT_TOP, but these are RelativeLayout-specific rules that apply to the RelativeLayout's coordinate system, NOT the FrameLayout's.
    // Line 605 - This creates RelativeLayout paramsRelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams(newWebViewWidth, newWebViewHeight);// But you're not updating the FrameLayout params at all!// The FrameLayout container was set in onCreateView with gravity, but resize() doesn't touch it
    Why this breaks:
    The initial layout in onCreateView() sets gravity on the FrameLayout.LayoutParams (lines 243-254)
    The resize() method sets gravity via RelativeLayout rules on the WebView
    These two systems could conflict or not work together as expected
    Fix: Need to ensure the resize logic accounts for the FrameLayout container gravity, or you need to update BOTH the FrameLayout params AND the WebView params consistently.
  2. Multiple applyWindowGravity() Calls May Cause Flickering
    Risk Level: 🔥 HIGH
    Issue: You're calling applyWindowGravity() in 3 places:
    onCreateDialog() - line 174
    onCreateView() - line 196
    onStart() - line 130
    Why this is risky:
    DialogFragments have a specific lifecycle. Setting window attributes multiple times could cause the dialog to reposition or flicker
    onStart() happens AFTER the dialog is shown, which means users might see the dialog jump to a different position
    Android's window system may not handle redundant gravity changes gracefully across different Android versions
    Likely bug: On some devices, you might see the in-app "pop" or shift position after it's already displayed.

and potentiall others.

IterableLogger.d(TAG, "Orientation changed, triggering resize");
runResizeScript();
}
}, 1500); // Increased delay for better stability
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants